﻿using System;
using System.Text;
using System.Linq;
using System.Xml.Linq;

using OpenTK.Graphics.ES20;

using Storyteller.Framework.Math;

namespace Storyteller.Framework.Graphics
{
	public class SpriteShader : IDisposable
	{
		public SpriteShader()
			: this(DefaultVertexShaderText, DefaultFragmentShaderText)
		{
		}

		public SpriteShader(string filePath)
		{
			XDocument xmlDocument = XDocument.Load(filePath);

			string vertexShader = (from f in xmlDocument.Descendants("VertexShader") select f).First().Value;
			string fragmentShader = (from f in xmlDocument.Descendants("FragmentShader") select f).First().Value;

			Initialize(vertexShader, fragmentShader);
		}

		public SpriteShader(string vertexShaderText, string fragmentShaderText)
		{
			Initialize(vertexShaderText, fragmentShaderText);
		}

		protected void Initialize(string vertexShaderText, string fragmentShaderText)
		{
			int vertexShader, fragmentShader;

			int logLength, status;

			vertexShader = GL.CreateShader(All.VertexShader);
		
			GL.ShaderSource(vertexShader, 1, new string[] { vertexShaderText }, (int[])null);
			GL.CompileShader(vertexShader);
			
			status = 0;
			GL.GetShader(vertexShader, All.CompileStatus, ref status);
			if(status == 0)
			{
				logLength = 0;
				GL.GetShader(vertexShader, All.InfoLogLength, ref logLength);
				if(logLength > 0)
				{
					StringBuilder logText = new StringBuilder(logLength);
					GL.GetShaderInfoLog(vertexShader, logLength, ref logLength, logText);
					Console.WriteLine("Vertex shader compilation failed:" + logText.ToString());
				}
				
				GL.DeleteShader(vertexShader);
				throw new InvalidOperationException("Failed to compile vertex shader");
			}
		
			fragmentShader = GL.CreateShader(All.FragmentShader);
		
			GL.ShaderSource(fragmentShader, 1, new string[] { fragmentShaderText }, (int[])null);
			GL.CompileShader(fragmentShader);
			
			status = 0;
			GL.GetShader(fragmentShader, All.CompileStatus, ref status);
			if(status == 0)
			{
				logLength = 0;
				GL.GetShader(fragmentShader, All.InfoLogLength, ref logLength);
				if(logLength > 0)
				{
					StringBuilder logText = new StringBuilder(logLength);
					GL.GetShaderInfoLog(fragmentShader, logLength, ref logLength, logText);
					Console.WriteLine("Fragment shader compilation failed:" + logText.ToString());
				}
				
				GL.DeleteShader(fragmentShader);
				throw new InvalidOperationException("Failed to compile fragment shader");
			}
			
			programId = GL.CreateProgram();	
			
			GL.AttachShader(programId, vertexShader);
			GL.AttachShader(programId, fragmentShader);

			GL.BindAttribLocation(programId, ShaderAttributes.Position, "position");
			GL.BindAttribLocation(programId, ShaderAttributes.TexCoord, "texcoord");
			GL.BindAttribLocation(programId, ShaderAttributes.Color, "color");
		
			GL.LinkProgram(programId);
		
			status = 0;
			GL.GetProgram(programId, All.LinkStatus, ref status);
			if(status == 0)
			{
				logLength = 0;
				GL.GetProgram(programId, All.InfoLogLength, ref logLength);
				if(logLength > 0)
				{
					StringBuilder logText = new StringBuilder(logLength);
					GL.GetProgramInfoLog(programId, logLength, ref logLength, logText);
					Console.WriteLine("Shader program link failed:" + logText.ToString());
				}
				
				GL.DeleteProgram(programId);
				throw new InvalidOperationException("Failed to link shader program");
			}

			uniforms[ShaderUniforms.TransformMatrix] = GL.GetUniformLocation(programId, "TransformMatrix");
			uniforms[ShaderUniforms.TextureSampler] = GL.GetUniformLocation(programId, "TextureSampler");

			GL.DeleteShader(fragmentShader);
			GL.DeleteShader(vertexShader);
		}

		public unsafe void Draw(Vertex[] vertices, int vertexCount, int glTextureId)
		{
			GL.UseProgram(programId);

			fixed (float* p = &worldMatrix.Row0.X)
			{
				GL.UniformMatrix4(uniforms[ShaderUniforms.TransformMatrix], 1, false, p);
			}

			GL.ActiveTexture(All.Texture0);
			GL.BindTexture(All.Texture2D, glTextureId);
			
			fixed(float *p = &vertices[0].x)
			{			
				GL.VertexAttribPointer(ShaderAttributes.Position, 2, All.Float, false, sizeof(Vertex), (IntPtr)p);
				GL.EnableVertexAttribArray(ShaderAttributes.Position);
				GL.VertexAttribPointer(ShaderAttributes.TexCoord, 2, All.Float, false, sizeof(Vertex), (IntPtr)(p + 2));
				GL.EnableVertexAttribArray(ShaderAttributes.TexCoord);
				GL.VertexAttribPointer(ShaderAttributes.Color, 4, All.Float, false, sizeof(Vertex), (IntPtr)(p + 4));
				GL.EnableVertexAttribArray(ShaderAttributes.Color);
				
				GL.DrawArrays(All.Triangles, 0, vertexCount);
			}
		}

		public void SetResolution(int screenWidth, int screenHeight)
		{
			if (screenWidth != width || screenHeight != height)
			{
				width = screenWidth;
				height = screenHeight;

				projectionMatrix = Matrix4.CreateOrthographicOffCenter(0, width, height, 0, 1, 1000);

				worldMatrix = modelMatrix * viewMatrix * projectionMatrix;
			}
		}

		public void Dispose()
		{
			Dispose(true);
			GC.SuppressFinalize(this);
		}

		protected virtual void Dispose(bool disposing)
		{
			if (!disposed)
			{
				if (disposing)
				{
					// dispose of managed resources
					uniforms = null;
				}

				// dispose of unmanaged resources
				GL.DeleteProgram(programId);

				disposed = true;
			}
		}

		class ShaderAttributes
		{
			public const int Position = 0;
			public const int Color = 1;
			public const int TexCoord = 2;
			public const int NumAttributes = 3;
		}

		class ShaderUniforms
		{
			public const int TransformMatrix = 0;
			public const int TextureSampler = 1;
			public const int NumUniforms = 2;
		}

		const string DefaultVertexShaderText = @"
			attribute vec4 position;
			attribute vec4 color;
			attribute vec2 texcoord;
			varying highp vec4 v_color;
			varying highp vec2 v_texcoord;
			uniform mat4 TransformMatrix;
			void main()
			{
				gl_Position = TransformMatrix * position;
				v_color = color;
				v_texcoord = texcoord;
			}";

		const string DefaultFragmentShaderText = @"
			varying highp vec4 v_color;
			varying highp vec2 v_texcoord;
			uniform sampler2D TextureSampler;
			void main()
			{
				gl_FragColor = texture2D(TextureSampler, v_texcoord) * v_color;
			}";

		int programId;
		int[] uniforms = new int[ShaderUniforms.NumUniforms];

		Matrix4 projectionMatrix = Matrix4.Identity;
		Matrix4 viewMatrix = Matrix4.Identity;
		Matrix4 modelMatrix = Matrix4.Identity;
		Matrix4 worldMatrix;

		int width = 0, height = 0;

		bool disposed = false;
	}
}

